# Part of Odoo. See LICENSE file for full copyright and licensing details.

from datetime import datetime

from odoo.http import Controller, request, route


class SaleProductConfiguratorController(Controller):

    @route(route='/sale/product_configurator/get_values', type='jsonrpc', auth='user', readonly=True)
    def sale_product_configurator_get_values(
        self,
        product_template_id,
        quantity,
        currency_id,
        so_date,
        product_uom_id=None,
        company_id=None,
        pricelist_id=None,
        ptav_ids=None,
        only_main_product=False,
        **kwargs,
    ):
        """Return all product information needed for the product configurator.

        :param int product_template_id: The product for which to seek information, as a
            `product.template` id.
        :param int quantity: The quantity of the product.
        :param int currency_id: The currency of the transaction, as a `res.currency` id.
        :param str so_date: The date of the `sale.order`, to compute the price at the right rate.
        :param int|None product_uom_id: The unit of measure of the product, as a `uom.uom` id.
        :param int|None company_id: The company to use, as a `res.company` id.
        :param int|None pricelist_id: The pricelist to use, as a `product.pricelist` id.
        :param list(int)|None ptav_ids: The combination of the product, as a list of
            `product.template.attribute.value` ids.
        :param bool only_main_product: Whether the optional products should be included or not.
        :param dict kwargs: Locally unused data passed to `_get_product_information`.
        :rtype: dict
        :return: A dict containing a list of products and a list of optional products information,
            generated by :meth:`_get_product_information`.
        """
        if company_id:
            request.update_context(allowed_company_ids=[company_id])
        product_template = self._get_product_template(product_template_id)

        combination = request.env['product.template.attribute.value']
        if ptav_ids:
            combination = request.env['product.template.attribute.value'].browse(ptav_ids).filtered(
                lambda ptav: ptav.product_tmpl_id.id == product_template_id
            )
            # Set missing attributes (unsaved no_variant attributes, or new attribute on existing product)
            unconfigured_ptals = (
                product_template.attribute_line_ids - combination.attribute_line_id).filtered(
                lambda ptal: ptal.attribute_id.display_type != 'multi')
            combination += unconfigured_ptals.mapped(
                lambda ptal: ptal.product_template_value_ids._only_active()[:1]
            )
        if not combination:
            combination = product_template._get_first_possible_combination()
        currency = request.env['res.currency'].browse(currency_id)
        pricelist = request.env['product.pricelist'].browse(pricelist_id)
        so_date = datetime.fromisoformat(so_date)

        return {
            'products': [
                dict(
                    **self._get_product_information(
                        product_template,
                        combination,
                        currency,
                        pricelist,
                        so_date,
                        quantity=quantity,
                        product_uom_id=product_uom_id,
                        **kwargs,
                    ),
                )
            ],
            'optional_products': [
                dict(
                    **self._get_product_information(
                        optional_product_template,
                        optional_product_template._get_first_possible_combination(
                            parent_combination=combination
                        ),
                        currency,
                        pricelist,
                        so_date,
                        # giving all the ptav of the parent product to get all the exclusions
                        parent_combination=product_template.attribute_line_ids.\
                            product_template_value_ids,
                        **kwargs,
                    ),
                    parent_product_tmpl_id=product_template.id,
                ) for optional_product_template in product_template.optional_product_ids if
                self._should_show_product(optional_product_template, combination)
            ] if not only_main_product else [],
            'currency_id': currency_id,
        }

    @route(
        route='/sale/product_configurator/create_product',
        type='jsonrpc',
        auth='user',
        methods=['POST'],
    )
    def sale_product_configurator_create_product(self, product_template_id, ptav_ids):
        """Create the product when there is a dynamic attribute in the combination.

        :param int product_template_id: The product for which to seek information, as a
            `product.template` id.
        :param list(int) ptav_ids: The combination of the product, as a list of
            `product.template.attribute.value` ids.
        :rtype: int
        :return: The product created, as a `product.product` id.
        """
        product_template = self._get_product_template(product_template_id)
        combination = request.env['product.template.attribute.value'].browse(ptav_ids)
        product = product_template._create_product_variant(combination)
        return product.id

    @route(
        route='/sale/product_configurator/update_combination',
        type='jsonrpc',
        auth='user',
        methods=['POST'],
        readonly=True,
    )
    def sale_product_configurator_update_combination(
        self,
        product_template_id,
        ptav_ids,
        currency_id,
        so_date,
        quantity,
        product_uom_id=None,
        company_id=None,
        pricelist_id=None,
        **kwargs,
    ):
        """Return the updated combination information.

        :param int product_template_id: The product for which to seek information, as a
            `product.template` id.
        :param list(int) ptav_ids: The combination of the product, as a list of
            `product.template.attribute.value` ids.
        :param int currency_id: The currency of the transaction, as a `res.currency` id.
        :param str so_date: The date of the `sale.order`, to compute the price at the right rate.
        :param int quantity: The quantity of the product.
        :param int|None product_uom_id: The unit of measure of the product, as a `uom.uom` id.
        :param int|None company_id: The company to use, as a `res.company` id.
        :param int|None pricelist_id:  The pricelist to use, as a `product.pricelist` id.
        :param dict kwargs: Locally unused data passed to `_get_basic_product_information`.
        :rtype: dict
        :return: Basic informations about a product, generated by
            :meth:`_get_basic_product_information`.
        """
        if company_id:
            request.update_context(allowed_company_ids=[company_id])
        product_template = self._get_product_template(product_template_id)
        pricelist = request.env['product.pricelist'].browse(pricelist_id)
        product_uom = request.env['uom.uom'].browse(product_uom_id)
        currency = request.env['res.currency'].browse(currency_id)
        combination = request.env['product.template.attribute.value'].browse(ptav_ids)
        product = product_template._get_variant_for_combination(combination)

        values = self._get_basic_product_information(
            product or product_template,
            pricelist,
            combination,
            quantity=quantity or 0.0,
            uom=product_uom,
            currency=currency,
            date=datetime.fromisoformat(so_date),
            **kwargs,
        )
        # Shouldn't be sent client-side
        values.pop('pricelist_rule_id', None)
        return values

    @route(
        route='/sale/product_configurator/get_optional_products',
        type='jsonrpc',
        auth='user',
        readonly=True,
    )
    def sale_product_configurator_get_optional_products(
        self,
        product_template_id,
        ptav_ids,
        parent_ptav_ids,
        currency_id,
        so_date,
        company_id=None,
        pricelist_id=None,
        **kwargs,
    ):
        """Return information about optional products for the given `product.template`.

        :param int product_template_id: The product for which to seek information, as a
            `product.template` id.
        :param list(int) ptav_ids: The combination of the product, as a list of
            `product.template.attribute.value` ids.
        :param list(int) parent_ptav_ids: The combination of the parent product, as a list of
            `product.template.attribute.value` ids.
        :param int currency_id: The currency of the transaction, as a `res.currency` id.
        :param str so_date: The date of the `sale.order`, to compute the price at the right rate.
        :param int|None company_id: The company to use, as a `res.company` id.
        :param int|None pricelist_id:  The pricelist to use, as a `product.pricelist` id.
        :param dict kwargs: Locally unused data passed to `_get_product_information`.
        :rtype: [dict]
        :return: A list of optional products information, generated by
            :meth:`_get_product_information`.
        """
        if company_id:
            request.update_context(allowed_company_ids=[company_id])
        product_template = self._get_product_template(product_template_id)
        parent_combination = request.env['product.template.attribute.value'].browse(
            parent_ptav_ids + ptav_ids
        )
        currency = request.env['res.currency'].browse(currency_id)
        pricelist = request.env['product.pricelist'].browse(pricelist_id)
        return [
            dict(
                **self._get_product_information(
                    optional_product_template,
                    optional_product_template._get_first_possible_combination(
                        parent_combination=parent_combination
                    ),
                    currency,
                    pricelist,
                    datetime.fromisoformat(so_date),
                    parent_combination=parent_combination,
                    **kwargs,
                ),
                parent_product_tmpl_id=product_template.id,
            ) for optional_product_template in product_template.optional_product_ids if
            self._should_show_product(optional_product_template, parent_combination)
        ]

    def _get_product_template(self, product_template_id):
        return request.env['product.template'].browse(product_template_id)

    def _get_product_information(
        self,
        product_template,
        combination,
        currency,
        pricelist,
        so_date,
        quantity=1,
        product_uom_id=None,
        parent_combination=None,
        show_packaging=True,
        **kwargs,
    ):
        """Return complete information about a product.

        :param product.template product_template: The product for which to seek information.
        :param product.template.attribute.value combination: The combination of the product.
        :param res.currency currency: The currency of the transaction.
        :param product.pricelist pricelist: The pricelist to use.
        :param datetime so_date: The date of the `sale.order`, to compute the price at the right
            rate.
        :param int quantity: The quantity of the product.
        :param int|None product_uom_id: The unit of measure of the product, as a `uom.uom` id.
        :param product.template.attribute.value|None parent_combination: The combination of the
            parent product.
        :param dict kwargs: Locally unused data passed to `_get_basic_product_information`.
        :rtype: dict
        :return: A dict with the following structure:
            {
                'product_tmpl_id': int,
                'id': int,
                'description_sale': str|False,
                'display_name': str,
                'price': float,
                'quantity': int
                'attribute_line': [{
                    'id': int
                    'attribute': {
                        'id': int
                        'name': str
                        'display_type': str
                    },
                    'attribute_value': [{
                        'id': int,
                        'name': str,
                        'price_extra': float,
                        'html_color': str|False,
                        'image': str|False,
                        'is_custom': bool
                    }],
                    'selected_attribute_id': int,
                }],
                'exclusions': dict,
                'archived_combination': dict,
                'parent_exclusions': dict,
                'available_uoms': dict (optional),
            }
        """
        uom = (
            (product_uom_id and request.env['uom.uom'].browse(product_uom_id))
            or product_template.uom_id
        )
        product = product_template._get_variant_for_combination(combination)
        attribute_exclusions = product_template._get_attribute_exclusions(
            parent_combination=parent_combination,
            combination_ids=combination.ids,
        )
        product_or_template = product or product_template
        ptals = product_template.attribute_line_ids
        attrs_map = {
            attr_data['id']: attr_data
            for attr_data in ptals.attribute_id.read(['id', 'name', 'display_type'])
        }
        ptavs = ptals.product_template_value_ids.filtered(lambda p: p.ptav_active or combination and p.id in combination.ids)
        ptavs_map = dict(zip(ptavs.ids, ptavs.read(['name', 'html_color', 'image', 'is_custom'])))

        values = dict(
            product_tmpl_id=product_template.id,
            **self._get_basic_product_information(
                product_or_template,
                pricelist,
                combination,
                quantity=quantity,
                uom=uom,
                currency=currency,
                date=so_date,
                **kwargs,
            ),
            quantity=quantity,
            uom=uom.read(['id', 'display_name'])[0],
            attribute_lines=[{
                'id': ptal.id,
                'attribute': dict(**attrs_map[ptal.attribute_id.id]),
                'attribute_values': [
                    dict(
                        **ptavs_map[ptav.id],
                        price_extra=self._get_ptav_price_extra(
                            ptav, currency, so_date, product_or_template
                        ),
                    ) for ptav in ptal.product_template_value_ids
                    if ptav.ptav_active or (combination and ptav.id in combination.ids)
                ],
                'selected_attribute_value_ids': combination.filtered(
                    lambda c: ptal in c.attribute_line_id
                ).ids,
                'create_variant': ptal.attribute_id.create_variant,
            } for ptal in product_template.attribute_line_ids],
            exclusions=attribute_exclusions['exclusions'],
            archived_combinations=attribute_exclusions['archived_combinations'],
            parent_exclusions=attribute_exclusions['parent_exclusions'],
        )
        if show_packaging and product_template._has_multiple_uoms():
            values['available_uoms'] = product_template._get_available_uoms().read(
                ['id', 'display_name']
            )
        # Shouldn't be sent client-side
        values.pop('pricelist_rule_id', None)
        return values

    def _get_basic_product_information(self, product_or_template, pricelist, combination, **kwargs):
        """Return basic information about a product.

        :param product.product|product.template product_or_template: The product for which to seek
            information.
        :param product.pricelist pricelist: The pricelist to use.
        :param product.template.attribute.value combination: The combination of the product.
        :param dict kwargs: Locally unused data passed to `_get_product_price`.
        :rtype: dict
        :return: A dict with the following structure:
            {
                'id': int,  # if product_or_template is a record of `product.product`.
                'description_sale': str|False,
                'display_name': str,
                'price': float,
            }
        """
        basic_information = dict(
            **product_or_template.read(['description_sale', 'display_name'])[0]
        )
        # If the product is a template, check the combination to compute the name to take dynamic
        # and no_variant attributes into account. Also, drop the id which was auto-included by the
        # search but isn't relevant since it is supposed to be the id of a `product.product` record.
        if not product_or_template.is_product_variant:
            basic_information['id'] = False
            combination_name = combination._get_combination_name()
            if combination_name:
                basic_information.update(
                    display_name=f"{basic_information['display_name']} ({combination_name})"
                )
        price, pricelist_rule_id = request.env['product.template']._get_configurator_display_price(
            product_or_template.with_context(
                **product_or_template._get_product_price_context(combination)
            ),
            pricelist=pricelist,
            **kwargs,
        )
        return dict(
            **basic_information,
            price=price,
            pricelist_rule_id=pricelist_rule_id,
            **request.env['product.template']._get_additional_configurator_data(
                product_or_template, pricelist=pricelist, **kwargs
            ),
        )

    def _get_ptav_price_extra(self, ptav, currency, date, product_or_template):
        """Return the extra price for a product template attribute value.

        :param product.template.attribute.value ptav: The product template attribute value for which
            to compute the extra price.
        :param res.currency currency: The currency to compute the extra price in.
        :param datetime date: The date to compute the extra price at.
        :param product.product|product.template product_or_template: The product on which the
            product template attribute value applies.
        :rtype: float
        :return: The extra price for the product template attribute value.
        """
        return ptav.currency_id._convert(
            ptav.price_extra,
            currency,
            request.env.company,
            date.date(),
        )

    def _should_show_product(self, product_template, parent_combination):
        """Decide whether a product should be shown in the configurator.

        :param product.template product_template: The product being checked.
        :param product.template.attribute.value parent_combination: The combination of the parent
            product.
        :rtype: bool
        :return: Whether the product should be shown in the configurator.
        """
        return True
